// Utilities for basic tasks in javascript.

$exists: function $exists(el) {
    return typeof el !== "undefined";
};

$fcd: function $fcd(obj, fn) {
    return function () { fn.apply(obj, arguments); }
};

$fcb: function $fcb(obj, fn, args) {
    return function () {
        args = args.concat(arguments);
        fn.apply(obj, args);
    }
};

$getEl = function $getEl(id) {
    return document.getElementById(id);
};

$addCssClass = function $addCssClass(el, cl) {
    //TODO: a spurious space sometimes appears at the beginning of className
    //avoid using classList for cross-browser support
    arr = el.className.split(" ");
    if (arr.indexOf(cl) === -1) {
        el.className += " " + cl;
    }
};

$removeCssClass = function $removeCssClass(el, cl) {
    //avoid using classList for cross-browser support
    arr = el.className.split(" ");
    index = arr.indexOf(cl);
    if (arr.indexOf(cl) !== -1) {
        arr.splice(index, 1);
        el.className = arr.join(" ");
    }
};

$addHandler = function $addHandler(el, event, del) {
    if (el.addEventListener) {
        el.addEventListener(event, del);
    }
    else if (el.attachEvent) {
        el.attachEvent(event, del);
    }
    else {
        throw "Element has no event listeners."
    }
};

$removeHandler = function $removeHandler(el, event, del) {
    if (el.removeEventListener) {
        el.removeEventListener(event, del);
    }
    else if (el.detachEvent) {
        el.detachEvent(event, del);
    }
    else {
        throw "Element has no event listeners."
    }
};

$setInheritance = function $setInheritance(base, child) {
    for (var key in base.prototype) {
        if (!child.prototype.key) {
            child.prototype.key = base.prototype.key
        }
    }
};

$getVerticalScroll = function $getVerticalScroll() {
    if (typeof (window.pageYOffset == 'number')) {
        return window.pageYOffset;
    }
    else if (document.body && document.body.scrollTop) { //note: sometimes when it just doesn't work, scrollTop is just permanently 0, so accept 0 as "false".
        return document.body.scrollTop;
    }
    else if (document.documentElement && document.documentElement.scrollTop) {
        return document.documentElement.scrollTop;
    }
    else {
        return 0;
    }
};

$setVerticalScroll = function $setVerticalScroll(val) {
    window.scrollTo(0, val);
};

$enableScrollBars = function $enableScrollBars() {
    if (document.documentElement) {
        document.documentElement.style.overflow = "auto";
    }

    if (document.body) {
        document.body.scroll = "yes";
    }
};

$disableScrollBars = function $disableScrollBars() {
    if (document.documentElement) {
        document.documentElement.style.overflow = "hidden";
    }

    if (document.body) {
        document.body.scroll = "no";
    }
};

$newDiv = function $newDiv(id, cssClasses) {
    var div = document.createElement("div");
    if ($exists(id)) {
        div.setAttribute("id", id);
    }
    if (Array.isArray(cssClasses)) {
        cssClasses.forEach(function (item) {
            $addCssClass(div, item);
        });
    }
    return div;
};

$arrayEquality = function $arrayEquality(arr1, arr2) {
    if (arr1.length !== arr2.length) { return false; }
    for (var i = 0; i < arr1.length; i++) {
        if (arr1[i] !== arr2[i]) { return false; }
    }
    return true;
};

$setCookie: function $setCookie(obj, expiration, path) {
    //adds the kvps from the object to the current cookie

    var kv;
    var cookie = "";
    for (var key in obj) { //set fresh values
        kv = key + "=" + encodeURIComponent(obj[key]) + ";";
        cookie += kv;
    }
    cookie += "expires=" + expiration + ";"; //need to handle these separately bc we don't want to encode
    cookie += "path=" + path + ";";
    document.cookie = cookie;
};

$readCookie: function $readCookie() {
    if (!document.cookie) { return {}; }

    var obj = {};
    var cookiePairs = document.cookie.split(";");
    var kv;
    for (var i = 0; i < cookiePairs.length; i++) {
        kv = cookiePairs[i].split("=");
        obj[kv[0]] = decodeURIComponent(kv[1]);
    }
    return obj;
};

$leftPad: function $leftPad(str) {
    str = str + "";
    return ("0000" + str).substring(str.length);
}

function Timer() {
    //handles tasks delayed by a fixed amount of time
    this._queue = [];
    this.__updateDel = $fcd(this, this._update);
};

Timer.prototype = {
    _queue: null,
    __timeout: 0,
    __timeoutId: null, //also doubles as a flag for whether this is currently running
    __updateDel: null,

    set: function set(callback, timeout) {
        this._queue.push({
            'callback': callback,
            'timeout': timeout
        });

        if (timeout < this.__timeout || this.__timeoutId === null) {
            this.__timeout = timeout;
            if (this.__timeoutId) {
                clearTimeout(this.__timeoutId);
            }
            this.__timeoutId = setTimeout(this.__updateDel, timeout);
        };
    },

    isQueued: function isQueued(callback) {
        var arr = this._queue.filter(el => el.callback === callback);
        return arr.length > 0;
    },

    clear: function clear() {
        this._queue = [];
        this.__timeout = 0;
        clearTimeout(this.__timeoutId);
        this.__timeoutId = null;
    },

    _update: function __update() {
        this.__runQueue();
        if (this._queue.length > 0) {
            timeout = this.__findTimeout();
            this.__timeout = timeout;
            this.__timeoutId = setTimeout(this.__updateDel, timeout);
        }
        else {
            this.__timeout = 0;
            this.__timeoutId = null;
        }
    },

    __runQueue: function __runQueue() {
        for (var i = 0; i < this._queue.length; i++) { //use boring old for fn bc we need "this" in scope
            var el = this._queue[i];
            el.timeout = el.timeout - this.__timeout;
            if (el.timeout <= 0) {
                el.callback();
                this._queue.splice(i, 1);
            }
        };
    },

    __findTimeout: function __findTimeout() {
        var timeouts = this._queue.map(el => el.timeout);
        return Math.min.apply(null, timeouts);
    },
};

function Sequencer() {
    //handles tasks that must be done in a certain order
    this._queue = [];
    this.__updateDel = $fcd(this, this._update);
    this.__listeners = [];
};

Sequencer.prototype = {
    _queue: null,
    __timeoutId: null, //also doubles as a flag for whether this is currently running
    __updateDel: null,
    __listeners: null,

    add: function add(callback, delay) {
        this._queue.push({
            'callback': callback,
            'delay': delay //time to wait after previous step is done before executing this one
        });

        if (this.__timeoutId === null) {
            this.__timeoutId = setTimeout(this.__updateDel, delay);
        };
    },
    
    declareBlock: function declareBlock(blockName) {
        this._queue.push(blockName);
    },

    removeBlock: function removeBlock(blockName) {
        // Removes the block of queued callbacks from the first block declaration of the given name, up to the next block declaration.
        // If no parameters are passed, removes the first block that isn't currently underway.
        // Returns true if a block was removed; false if no such block is queued.
        var start = -1;
        var end = -1;
        var i = 0;
        while (i < this._queue.length && start === -1) {
            if (blockName) {
                if (this._queue[i] === blockName) { start = i; }
            }
            else {
                if (this.__isBlockLabel(this._queue[i])) { start = i; }
            }
            i++;
        }
        while (i < this._queue.length && end === -1) {
            if (this.__isBlockLabel(this._queue[i])) { end = i; }
            i++;
        }
        if (start !== -1) {
            //block found
            if (end !== -1) {
                //there is a subsequent block declaration
                this._queue.splice(start, (end - start));
            }
            else {
                //this block takes up the whole rest of the queue
                this._queue = this._queue.slice(0, start);
            }
            return true;
        }
        else {
            return false;
        }
    },

    removeBlocksOtherThan: function removeBlocksOtherThan(blockName) {
        // Removes all queued blocks of callbacks other than the first one with the given name.
        // Note that any singletons at the front of the queue will still be processed.

        // Returns 1 if the block was the first block to appear; -1 if the block was not the first block to appear; 0 if nothing was removed

        debugger;
        var preserveStart = -1;
        var preserveEnd = -1;
        var cutStart = -1;
        var i = 0;
        
        while (i < this._queue.length && (preserveStart === -1 || cutStart === -1 || preserveEnd === -1)) {
            if (preserveStart === -1 && this._queue[i] === blockName) { preserveStart = i; }
            else if (this.__isBlockLabel(this._queue[i])) {
                if (preserveStart !== -1 && preserveEnd === -1) {
                    preserveEnd = i;
                }
                if (cutStart === -1) {
                    cutStart = i;
                }
            }
            i++;
        }

        // there are two distinct cases:
        // 1) xxx ______ xxxxx _______ <-- unwanted blocks exist between singletons and target block
        // 2) xxx xxxx _______________ <-- target block is right after singletons

        var somethingCut = 0;
        if (preserveEnd !== -1) { //either way, we can cut off the tail (if it exists)
            this._queue = this._queue.slice(0, preserveEnd);
            somethingCut = 1;
        } //if we're in case 2, we're done

        if (cutStart < preserveStart || preserveStart === -1) {
            this._queue.splice(cutStart, (preserveStart - cutStart));
            somethingCut = -1;
        }

        return somethingCut;
    },

    clear: function clear() {
        this._queue = [];
        clearTimeout(this.__timeoutId);
        this.__timeoutId = null;
    },

    addBlockListener: function addBlockListener(del) {
        //todo: theoretically this api should include something to remove the listener as well, but I have no need for that.
        this.__listeners.push(del);
    },

    hasQueuedItems: function hasQueuedItems() {
        return !!this.__nextQueueItem();
    },

    __raiseBlockEvent: function __raiseBlockEvent(blockName) {
        this.__listeners.forEach(function (el) {
            el.call(null, blockName);
        });
    },

    _update: function __update() {
        var timeout = 0;
        
        //handle next step
        while (timeout <= 0 && this._queue.length > 0) {
            if (this.__isBlockLabel(this._queue[0])) {
                this.__raiseBlockEvent(this._queue[0]);
                this._queue.splice(0, 1);
                //timeout = 0;
                continue;
            }
            else { //this is an actual item in the queue
                //execute step
                this._queue[0].callback();
                this._queue.splice(0, 1);

                //check next step's delay
                if (this._queue.length > 0) {
                    var next = this.__nextQueueItem();
                    if (next) {
                        timeout = next.delay;
                    } else {
                        timeout = 0;
                    }
                }
            }
        }

        if (timeout > 0 && this._queue.length > 0) {
            this.__timeoutId = setTimeout(this.__updateDel, timeout);
        }
        else {
            this.__timeoutId = null;
        }
    },

    __isBlockLabel: function __isBlockLabel(queueItem) {
        return typeof queueItem === "string";
    },

    __nextQueueItem: function __nextQueueItem() {
        for (var i = 0; i < this._queue.length; i++) {
            if (!this.__isBlockLabel(this._queue[i])) { 
                return this._queue[i];
            }
        }
        return null;
    },

};